package cool.houge.pig.infra.service.token;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import cool.houge.pig.PigProps;
import cool.houge.pig.model.AuthClient;
import cool.houge.pig.model.Token;
import cool.houge.pig.model.User;
import cool.houge.pig.repository.auth.TokenRepository;
import cool.houge.pig.service.SharedSequenceService;
import cool.houge.pig.service.auth.AuthTokenException;
import cool.houge.pig.service.auth.AuthTokenException.Error;
import cool.houge.pig.service.auth.AuthTokenInput;
import cool.houge.pig.service.auth.AuthTokenOutput;
import cool.houge.pig.service.auth.BuildTokenMeta;
import cool.houge.pig.service.auth.SharedAuthService;
import cool.houge.pig.service.auth.SharedTokenService;
import cool.houge.pig.service.auth.TokenPreprocess;
import cool.houge.pig.service.auth.TokenService;
import cool.houge.pig.service.webhook.SharedWebhookService;
import cool.houge.pig.service.webhook.WebhookInput;
import cool.houge.pig.service.webhook.WebhookType;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuples;

/**
 * 令牌实现.
 *
 * @author KK (kzou227@qq.com)
 */
@Service
public class TokenServiceImpl implements SharedTokenService, TokenService {

  private final PigProps pigProps;
  private final List<TokenPreprocess> tokenPreprocesses;
  private final SharedAuthService authService;
  private final SharedSequenceService sequenceService;
  private final Algorithm algorithm;
  private final TokenRepository tokenRepository;
  private final SharedWebhookService webhookService;

  /**
   * 构造对象.
   *
   * @param pigProps
   * @param tokenPreprocessProvider 令牌生成对象提供者
   * @param authService
   * @param sequenceService
   * @param algorithm
   * @param tokenRepository
   * @param webhookService
   */
  public TokenServiceImpl(
      PigProps pigProps,
      ObjectProvider<TokenPreprocess> tokenPreprocessProvider,
      SharedAuthService authService,
      SharedSequenceService sequenceService,
      Algorithm algorithm,
      TokenRepository tokenRepository,
      SharedWebhookService webhookService) {
    this.pigProps = pigProps;
    this.tokenPreprocesses = tokenPreprocessProvider.stream().collect(Collectors.toList());
    this.authService = authService;
    this.sequenceService = sequenceService;
    this.algorithm = algorithm;
    this.tokenRepository = tokenRepository;
    this.webhookService = webhookService;
  }

  @Override
  public Mono<AuthTokenOutput> buildToken(BuildTokenMeta meta) {
    return sequenceService
        .nextTokenSeq()
        .map(
            tokenId -> {
              var user = meta.getUser();
              var authClient = meta.getAuthClient();

              var serialNumber = String.valueOf(tokenId);
              var accessToken = buildAccessToken(serialNumber, user);
              var refreshToken = buildRefreshToken(serialNumber, user, authClient);
              return new Token(true)
                  .setId(tokenId)
                  .setUid(user.getId())
                  .setAccessToken(accessToken)
                  .setRefreshToken(refreshToken)
                  .setClientId(authClient.getClientId())
                  .setGrantType(meta.getGrantType())
                  .setScope(meta.getScope());
            })
        .flatMap(
            token -> {
              var output =
                  new AuthTokenOutput()
                      .setAccessToken(token.getAccessToken())
                      .setRefreshToken(token.getRefreshToken())
                      .setExpiresIn(pigProps.getToken().getAccessTokenExpireDuration().toSeconds());
              return tokenRepository
                  .save(token)
                  // 执行登录的 Webhook 回调
                  .then(invokeLoginWebhook(meta.getUser(), meta.getAuthClient()))
                  .thenReturn(output);
            });
  }

  @Transactional
  @Override
  public Mono<AuthTokenOutput> obtainToken(AuthTokenInput input) {
    return Flux.fromIterable(tokenPreprocesses)
        .filter(
            tokenPreprocess -> Objects.equals(input.getGrantType(), tokenPreprocess.grantType()))
        .single()
        .onErrorResume(
            NoSuchElementException.class,
            t -> {
              var msg = Strings.lenientFormat("不支持的授权类型[%s]", input.getGrantType());
              return Mono.error(new AuthTokenException(Error.UNSUPPORTED_GRANT_TYPE, msg));
            })
        .zipWith(
            authService.obtainAuthClient(
                input.getClientId(), input.getClientSecret(), input.getGrantType()))
        .flatMap(
            t -> {
              var tokenPreprocess = t.getT1();
              return tokenPreprocess
                  .process(t.getT2(), input)
                  .map(o -> Tuples.of(t.getT1(), t.getT2(), o));
            })
        .flatMap(
            t -> {
              return buildToken(
                  BuildTokenMeta.builder()
                      .authClient(t.getT2())
                      .user(t.getT3().getUser())
                      .grantType(input.getGrantType())
                      .scope(input.getScope())
                      .build());
            });
  }

  @VisibleForTesting
  Mono<Void> invokeLoginWebhook(User user, AuthClient authClient) {
    var metadata = WebhookInput.Metadata.builder().build();
    var body =
        WebhookInput.Body.builder().uid(user.getId()).clientId(authClient.getClientId()).build();
    var input =
        WebhookInput.builder().type(WebhookType.LOGIN).metadata(metadata).body(body).build();
    return webhookService.invoke(input);
  }

  private String buildAccessToken(String serialNumber, User user) {
    return JWT.create()
        .withJWTId(serialNumber)
        .withClaim("uid", user.getId())
        .withExpiresAt(
            Date.from(Instant.now().plus(pigProps.getToken().getAccessTokenExpireDuration())))
        .sign(algorithm);
  }

  private String buildRefreshToken(String serialNumber, User user, AuthClient authClient) {
    return JWT.create()
        .withJWTId(serialNumber)
        .withClaim("uid", user.getId())
        .withClaim("client_id", authClient.getClientId())
        .withExpiresAt(
            Date.from(Instant.now().plus(pigProps.getToken().getRefreshTokenExpireDuration())))
        .sign(algorithm);
  }
}
